package de.mmis.core.sexpression;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import de.mmis.core.base.Toolbox;
import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.LeafNode.Encoding;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.Tree.Type;
import de.mmis.core.base.abstracttree.TreeWriter;

/**
 * This class is providing static methods to write a Tree into an output stream
 * or byte array using S-Expression syntax
 * 
 * @author Christian Koch
 */
public class SExpressionWriter implements TreeWriter {
	/**
	 * converts given Tree into a byte array
	 * 
	 * @param exp
	 *            Tree to be converted
	 * @return byte array representation of given S-Expression
	 */
	public static byte[] writeSExpression(Tree exp) {
		return writeSExpression(exp, " ");
	}

	/**
	 * converts given Tree into a byte array
	 * 
	 * @param exp
	 *            Tree to be converted
	 * @return byte array representation of given Tree
	 */
	public static byte[] writeSExpression(Tree exp, String structSeparator) {
		ByteArrayOutputStream os = new ByteArrayOutputStream();
		try {
			writeSExpressionInternal(exp, os, structSeparator);
		} catch (IOException e) {
			// cannot happen
			e.printStackTrace();
		}
		return os.toByteArray();
	}

	/**
	 * writes given Tree into an output stream
	 * 
	 * @param exp
	 *            Tree to write
	 * @param out
	 *            output stream to write to
	 * @param buffered
	 *            if set to true, a BufferedOutputStream will be created and the
	 *            SExpression will be sent buffered
	 * @param newLine
	 *            append newline character LF after writing S-Expression
	 * @throws IOException
	 *             error while writing to output stream
	 */
	public static void writeSExpression(Tree exp, OutputStream out,
			boolean buffered, boolean newLine) throws IOException {
		writeSExpression(exp, out, buffered, newLine, " ");
	}

	/**
	 * writes given Tree into an output stream
	 * 
	 * @param exp
	 *            Tree to write
	 * @param out
	 *            output stream to write to
	 * @param buffered
	 *            if set to true, a BufferedOutputStream will be created and the
	 *            SExpression will be sent buffered
	 * @param newLine
	 *            append newline character LF after writing S-Expression
	 * @throws IOException
	 *             error while writing to output stream
	 */
	public static void writeSExpression(Tree exp, OutputStream out,
			boolean buffered, boolean newLine, String structSeparator)
			throws IOException {
		if (buffered) {
			BufferedOutputStream bout = new BufferedOutputStream(out);
			writeSExpressionInternal(exp, bout, structSeparator);
			if (newLine)
				bout.write('\n');
			bout.flush();
		} else {
			writeSExpressionInternal(exp, out, structSeparator);
			if (newLine)
				out.write('\n');
		}
	}

	/**
	 * writes given Tree into an output stream
	 * 
	 * @param exp
	 *            Tree to write
	 * @param out
	 *            output stream to write to
	 * @throws IOException
	 *             error while writing to output stream
	 */
	private static void writeSExpressionInternal(Tree exp, OutputStream out,
			String structSeparator) throws IOException {

		if (exp.getType() == Type.INNER) {
			out.write('(');
			for (int i = 0; i < ((InnerNode) exp).getNumberOfSubTrees(); i++) {
				if (i > 0) {
					for (int j = 0; j < structSeparator.length(); j++)
						out.write(structSeparator.charAt(j));
				}
				writeSExpressionInternal(((InnerNode) exp).getSubTree(i), out,
						structSeparator);
			}
			out.write(')');
		} else if (exp.getType() == Type.LEAF) {
			/*
			 * if (((LeafNode) exp).getDisplayHintData() != null) {
			 * out.write('['); writeAtomData(out, ((LeafNode)
			 * exp).getDisplayHintData(), ((LeafNode)
			 * exp).getDisplayHintEncoding()); out.write(']'); }
			 */
			writeAtomData(out, ((LeafNode) exp).getData(),
					((LeafNode) exp).getEncoding());
		}
	}

	/**
	 * auxiliary function to write atomic data to output stream using the given
	 * encoding
	 * 
	 * @param out
	 *            output stream to write to
	 * @param data
	 *            data to encode and write
	 * @param encoding
	 *            encoding to be used
	 * @throws IOException
	 *             error while writing to output stream
	 */
	private static void writeAtomData(OutputStream out, byte[] data,
			Encoding encoding) throws IOException {
		switch (encoding) {
		case Base64:
			out.write('|');
			writeBase64(out, data);
			out.write('|');
			break;
		case Hex:
			out.write('#');
			for (int i = 0; i < data.length; i++) {
				int b1 = (data[i] & 0xff) / 16;
				b1 += (b1 < 10 ? '0' : 'A' - 10);
				int b2 = (data[i] & 0xff) % 16;
				b2 += (b2 < 10 ? '0' : 'A' - 10);
				out.write(b1);
				out.write(b2);
			}
			out.write('#');
			break;
		case Quoted:
			out.write('"');
			for (int i = 0; i < data.length; i++) {
				switch (data[i]) {
				case '\n':
					out.write('\\');
					out.write('n');
					break;
				case '\t':
					out.write('\\');
					out.write('t');
					break;
				case '\r':
					out.write('\\');
					out.write('r');
					break;
				case '\f':
					out.write('\\');
					out.write('f');
					break;
				case '\b':
					out.write('\\');
					out.write('b');
					break;
				case 0xB: // '\v' (vertical tab)
					out.write('\\');
					out.write('v');
					break;
				case '"':
					out.write('\\');
					out.write('"');
					break;
				case '\\':
					out.write('\\');
					out.write('\\');
					break;
				default:
					if (data[i] > 0x1F && data[i] < 0x7F) // printable ascii
															// chars
						out.write(data[i]);
					else // escape not printable chars
					{
						out.write('\\');
						out.write(((data[i] & 0xff) / 100) + '0');
						out.write((((data[i] & 0xff) % 100) / 10) + '0');
						out.write(((data[i] & 0xff) % 10) + '0');
					}
					break;
				}
			}
			out.write('"');
			break;
		case Token:
			out.write(data);
			break;
		case Verbatim:
			out.write(Toolbox.toUTF8Bytes(Integer.toString(data.length)));
			out.write(':');
			out.write(data);
			break;
		}
	}

	// Mapping table from 6-bit nibbles to Base64 characters.
	private static byte[] map1 = new byte[64];
	static {
		int i = 0;
		for (byte c = 'A'; c <= 'Z'; c++)
			map1[i++] = c;
		for (byte c = 'a'; c <= 'z'; c++)
			map1[i++] = c;
		for (byte c = '0'; c <= '9'; c++)
			map1[i++] = c;
		map1[i++] = '+';
		map1[i++] = '/';
	}

	/**
	 * encodes given byte array to base64 and write it to output stream
	 * 
	 * @param out
	 *            {@link OutputStream} to write base64 data to
	 * @param in
	 *            byte array to encode
	 * @throws IOException
	 */
	public static void writeBase64(OutputStream out, byte[] in)
			throws IOException {
		int iLen = in.length;
		int oDataLen = (iLen * 4 + 2) / 3; // output length without padding
		// int oLen = ((iLen + 2) / 3) * 4; // output length including padding
		// byte[] out = new byte[oLen];
		int ip = 0;
		int op = 0;
		while (ip < in.length) {
			int i0 = in[ip++] & 0xff;
			int i1 = ip < in.length ? in[ip++] & 0xff : 0;
			int i2 = ip < in.length ? in[ip++] & 0xff : 0;
			int o0 = i0 >>> 2;
			int o1 = ((i0 & 3) << 4) | (i1 >>> 4);
			int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6);
			int o3 = i2 & 0x3F;
			out.write(map1[o0]);
			op++;
			out.write(map1[o1]);
			op++;
			out.write(op < oDataLen ? map1[o2] : (byte) '=');
			op++;
			out.write(op < oDataLen ? map1[o3] : (byte) '=');
			op++;
		}
	}

	@Override
	public byte[] writeAbstractTree(Tree abstractTree) {
		return writeSExpression(abstractTree);
	}

	@Override
	public void writeAbstractTree(Tree abstractTree, OutputStream out)
			throws IOException {
		writeSExpression(abstractTree, out, false, false);
	}
}
